/*!
 * Shuffle.js by @Vestride
 * Categorize, sort, and filter a responsive grid of items.
 * Dependencies: jQuery 1.9+, Modernizr 2.6.2+
 * @license MIT license
 * @version 2.1.1
 */
(function (factory) {
    if (typeof define === 'function' && define.amd) {
        define(['jquery', 'modernizr'], factory);
    } else {
        factory(window.$, window.Modernizr);
    }
})(function($, Modernizr, undefined) {

    'use strict';


// Validate Modernizr exists.
// Shuffle requires `csstransitions`, `csstransforms`, `csstransforms3d`,
// and `prefixed` to exist on the Modernizr object.
    if (typeof Modernizr !== 'object') {
        throw new Error('Shuffle.js requires Modernizr.\n' +
            'http://vestride.github.io/Shuffle/#dependencies');
    }


    /**
     * Returns css prefixed properties like `-webkit-transition` or `box-sizing`
     * from `transition` or `boxSizing`, respectively.
     * @param {(string|boolean)} prop Property to be prefixed.
     * @return {string} The prefixed css property.
     */
    function dashify( prop ) {
        if (!prop) {
            return '';
        }

        // Replace upper case with dash-lowercase,
        // then fix ms- prefixes because they're not capitalized.
        return prop.replace(/([A-Z])/g, function( str, m1 ) {
            return '-' + m1.toLowerCase();
        }).replace(/^ms-/,'-ms-');
    }

// Constant, prefixed variables.
    var TRANSITION = Modernizr.prefixed('transition');
    var TRANSITION_DELAY = Modernizr.prefixed('transitionDelay');
    var TRANSITION_DURATION = Modernizr.prefixed('transitionDuration');
    var TRANSITIONEND = {
        'WebkitTransition' : 'webkitTransitionEnd',
        'transition' : 'transitionend'
    }[ TRANSITION ];
    var TRANSFORM = Modernizr.prefixed('transform');
    var CSS_TRANSFORM = dashify(TRANSFORM);

// Constants
    var CAN_TRANSITION_TRANSFORMS = Modernizr.csstransforms && Modernizr.csstransitions;
    var HAS_TRANSFORMS_3D = Modernizr.csstransforms3d;
    var SHUFFLE = 'shuffle';

// Configurable. You can change these constants to fit your application.
// The default scale and concealed scale, however, have to be different values.
    var ALL_ITEMS = 'all';
    var FILTER_ATTRIBUTE_KEY = 'groups';
    var DEFAULT_SCALE = 1;
    var CONCEALED_SCALE = 0.001;


// Underscore's throttle function.
    function throttle(func, wait, options) {
        var context, args, result;
        var timeout = null;
        var previous = 0;
        options = options || {};
        var later = function() {
            previous = options.leading === false ? 0 : $.now();
            timeout = null;
            result = func.apply(context, args);
            context = args = null;
        };
        return function() {
            var now = $.now();
            if (!previous && options.leading === false) {
                previous = now;
            }
            var remaining = wait - (now - previous);
            context = this;
            args = arguments;
            if (remaining <= 0 || remaining > wait) {
                clearTimeout(timeout);
                timeout = null;
                previous = now;
                result = func.apply(context, args);
                context = args = null;
            } else if (!timeout && options.trailing !== false) {
                timeout = setTimeout(later, remaining);
            }
            return result;
        };
    }

// Used for unique instance variables
    var id = 0;


    /**
     * Categorize, sort, and filter a responsive grid of items.
     *
     * @param {Element|jQuery} element An element or a jQuery collection which
     *     is the parent container for the grid items.
     * @param {Object} [options=Shuffle.options] Options object.
     * @constructor
     */
    var Shuffle = function( element, options ) {
        options = options || {};
        $.extend( this, Shuffle.options, options, Shuffle.settings );

        this.$el = $(element);
        this.$window = $(window);
        this.unique = 'shuffle_' + id++;

        this._fire( Shuffle.EventType.LOADING );
        this._init();

        // Dispatch the done event asynchronously so that people can bind to it after
        // Shuffle has been initialized.
        setTimeout( $.proxy(function() {
            this.initialized = true;
            this._fire( Shuffle.EventType.DONE );
        }, this), 16 );
    };


    /**
     * Events the container element emits with the .shuffle namespace.
     * For example, "done.shuffle".
     * @enum {string}
     */
    Shuffle.EventType = {
        LOADING: 'loading',
        DONE: 'done',
        SHRINK: 'shrink',
        SHRUNK: 'shrunk',
        FILTER: 'filter',
        FILTERED: 'filtered',
        SORTED: 'sorted',
        LAYOUT: 'layout',
        REMOVED: 'removed'
    };


    Shuffle.prototype = {

        _init : function() {
            var self = this,
                containerCSS,
                containerWidth,
                resizeFunction = $.proxy( self._onResize, self ),
                debouncedResize = self.throttle ?
                    self.throttle( resizeFunction, self.throttleTime ) :
                    resizeFunction,
                sort = self.initialSort ? self.initialSort : null;


            self._layoutList = [];
            self._shrinkList = [];

            self._setVars();

            // Zero out all columns
            self._resetCols();

            // Add classes and invalidate styles
            self._addClasses();

            // Set initial css for each item
            self._initItems();

            // Bind resize events
            // http://stackoverflow.com/questions/1852751/window-resize-event-firing-in-internet-explorer
            self.$window.on('resize.' + SHUFFLE + '.' + self.unique, debouncedResize);

            // Get container css all in one request. Causes reflow
            containerCSS = self.$el.css(['paddingLeft', 'paddingRight', 'position']);
            containerWidth = self._getOuterWidth( self.$el[0] );

            // Position cannot be static.
            if ( containerCSS.position === 'static' ) {
                self.$el[0].style.position = 'relative';
            }

            // Get offset from container
            self.offset = {
                left: parseInt( containerCSS.paddingLeft, 10 ) || 0,
                top: parseInt( containerCSS.paddingTop, 10 ) || 0
            };

            // We already got the container's width above, no need to cause another reflow getting it again...
            // Calculate the number of columns there will be
            self._setColumns( parseInt( containerWidth, 10 ) );

            // Kick off!
            self.shuffle( self.group, sort );

            // The shuffle items haven't had transitions set on them yet
            // so the user doesn't see the first layout. Set them now that the first layout is done.
            if ( self.supported ) {
                setTimeout(function() {
                    self._setTransitions();
                    self.$el[0].style[ TRANSITION ] = 'height ' + self.speed + 'ms ' + self.easing;
                }, 0);
            }
        },

        // Will invalidate styles
        _addClasses : function() {
            this.$el.addClass( SHUFFLE );
            this.$items.addClass('shuffle-item filtered');
        },

        _setVars : function() {
            var self = this,
                columnWidth = self.columnWidth;

            self.$items = self._getItems();

            // Column width is the default setting and sizer is not (meaning passed in)
            // Assume they meant column width to be the sizer
            if ( columnWidth === 0 && self.sizer !== null ) {
                columnWidth = self.sizer;
            }

            // If column width is a string, treat is as a selector and search for the
            // sizer element within the outermost container
            if ( typeof columnWidth === 'string' ) {
                self.$sizer = self.$el.find( columnWidth );

                // Check for an element
            } else if ( columnWidth && columnWidth.nodeType && columnWidth.nodeType === 1 ) {
                // Wrap it in jQuery
                self.$sizer = $( columnWidth );

                // Check for jQuery object
            } else if ( columnWidth && columnWidth.jquery ) {
                self.$sizer = columnWidth;
            }

            if ( self.$sizer && self.$sizer.length ) {
                self.useSizer = true;
                self.sizer = self.$sizer[0];
            }
        },


        /**
         * Filter the elements by a category.
         * @param {string} [category] Category to filter by. If it's given, the last
         *     category will be used to filter the items.
         * @param {jQuery} [$collection] Optionally filter a collection. Defaults to
         *     all the items.
         * @return {jQuery} Filtered items.
         */
        _filter : function( category, $collection ) {
            var self = this,
                isPartialSet = $collection !== undefined,
                $items = isPartialSet ? $collection : self.$items,
                $filtered = $();

            category = category || self.lastFilter;

            self._fire( Shuffle.EventType.FILTER );

            // Loop through each item and use provided function to determine
            // whether to hide it or not.
            if ( $.isFunction( category ) ) {
                $items.each(function() {
                    var $item = $(this);
                    if ( category.call($item[0], $item, self) ) {
                        $filtered = $filtered.add( $item );
                    }
                });

                // Otherwise we've been passed a category to filter by
            } else {
                self.group = category;

                // category === 'all', add filtered class to everything
                if ( category === ALL_ITEMS ) {
                    $filtered = $items;

                    // Check each element's data-groups attribute against the given category.
                } else {
                    $items.each(function() {
                        var $item = $(this),
                            groups = $item.data( FILTER_ATTRIBUTE_KEY ),
                            keys = self.delimeter && !$.isArray( groups ) ?
                                groups.split( self.delimeter ) :
                                groups;

                        if ( $.inArray(category, keys) > -1 ) {
                            $filtered = $filtered.add( $item );
                        }
                    });
                }
            }

            // Individually add/remove concealed/filtered classes
            self._toggleFilterClasses( $items, $filtered );

            $items = null;
            $collection = null;

            return $filtered;
        },


        _toggleFilterClasses : function( $items, $filtered ) {
            var concealed = 'concealed',
                filtered = 'filtered';

            $items.filter( $filtered ).each(function() {
                var $filteredItem = $(this);
                // Remove concealed if it's there
                if ( $filteredItem.hasClass( concealed ) ) {
                    $filteredItem.removeClass( concealed );
                }
                // Add filtered class if it's not there
                if ( !$filteredItem.hasClass( filtered ) ) {
                    $filteredItem.addClass( filtered );
                }
            });

            $items.not( $filtered ).each(function() {
                var $filteredItem = $(this);
                // Add concealed if it's not there
                if ( !$filteredItem.hasClass( concealed ) ) {
                    $filteredItem.addClass( concealed );
                }
                // Remove filtered class if it's there
                if ( $filteredItem.hasClass( filtered ) ) {
                    $filteredItem.removeClass( filtered );
                }
            });
        },

        /**
         * Set the initial css for each item
         * @param {jQuery} [$items] Optionally specifiy at set to initialize
         */
        _initItems : function( $items ) {
            $items = $items || this.$items;
            $items.css( this.itemCss ).data('position', {x: 0, y: 0});
        },

        _updateItemCount : function() {
            this.visibleItems = this.$items.filter('.filtered').length;
        },

        _setTransition : function( element ) {
            element.style[ TRANSITION ] = CSS_TRANSFORM + ' ' + this.speed + 'ms ' + this.easing + ', opacity ' + this.speed + 'ms ' + this.easing;
        },

        _setTransitions : function( $items ) {
            var self = this;

            $items = $items || self.$items;
            $items.each(function() {
                self._setTransition( this );
            });
        },

        _setSequentialDelay : function( $collection ) {
            var self = this;

            if ( !self.supported ) {
                return;
            }

            // $collection can be an array of dom elements or jquery object
            $.each($collection, function(i, el) {
                // This works because the transition-property: transform, opacity;
                el.style[ TRANSITION_DELAY ] = '0ms,' + ((i + 1) * self.sequentialFadeDelay) + 'ms';

                // Set the delay back to zero after one transition
                $(el).on(TRANSITIONEND + '.' + self.unique, function(evt) {
                    var target = evt.currentTarget;
                    if ( target === evt.target ) {
                        target.style[ TRANSITION_DELAY ] = '0ms';
                        $(target).off(TRANSITIONEND + '.' + self.unique);
                    }
                });
            });
        },

        _getItems : function() {
            return this.$el.children( this.itemSelector );
        },

        _getPreciseDimension : function( element, style ) {
            var dimension;
            if ( window.getComputedStyle ) {
                dimension = window.getComputedStyle( element, null )[ style ];
            } else {
                dimension = $( element ).css( style );
            }
            return parseFloat( dimension );
        },


        /**
         * Returns the outer width of an element, optionally including its margins.
         * @param {Element} element The element.
         * @param {boolean} [includeMargins] Whether to include margins. Default is false.
         * @return {number} The width.
         */
        _getOuterWidth : function( element, includeMargins ) {
            var width = element.offsetWidth;

            // Use jQuery here because it uses getComputedStyle internally and is
            // cross-browser. Using the style property of the element will only work
            // if there are inline styles.
            if (includeMargins) {
                var styles = $(element).css(['marginLeft', 'marginRight']);

                // Defaults to zero if parsing fails because IE will return 'auto' when
                // the element doesn't have margins instead of the computed style.
                var marginLeft = parseFloat(styles.marginLeft) || 0;
                var marginRight = parseFloat(styles.marginRight) || 0;
                width += marginLeft + marginRight;
            }

            return width;
        },


        /**
         * Returns the outer height of an element, optionally including its margins.
         * @param {Element} element The element.
         * @param {boolean} [includeMargins] Whether to include margins. Default is false.
         * @return {number} The height.
         */
        _getOuterHeight : function( element, includeMargins ) {
            var height = element.offsetHeight;

            if (includeMargins) {
                var styles = $(element).css(['marginTop', 'marginBottom']);
                var marginTop = parseFloat(styles.marginTop) || 0;
                var marginBottom = parseFloat(styles.marginBottom) || 0;
                height += marginTop + marginBottom;
            }

            return height;
        },


        _getColumnSize : function( gutterSize, containerWidth ) {
            var size;

            // If the columnWidth property is a function, then the grid is fluid
            if ( $.isFunction( this.columnWidth ) ) {
                size = this.columnWidth(containerWidth);

                // columnWidth option isn't a function, are they using a sizing element?
            } else if ( this.useSizer ) {
                size = this._getPreciseDimension(this.sizer, 'width');

                // if not, how about the explicitly set option?
            } else if ( this.columnWidth ) {
                size = this.columnWidth;

                // or use the size of the first item
            } else if ( this.$items.length > 0 ) {
                size = this._getOuterWidth(this.$items[0], true);

                // if there's no items, use size of container
            } else {
                size = containerWidth;
            }

            // Don't let them set a column width of zero.
            if ( size === 0 ) {
                size = containerWidth;
            }

            return size + gutterSize;
        },


        _getGutterSize : function( containerWidth ) {
            var size;
            if ( $.isFunction( this.gutterWidth ) ) {
                size = this.gutterWidth(containerWidth);
            } else if ( this.useSizer ) {
                size = this._getPreciseDimension(this.sizer, 'marginLeft');
            } else {
                size = this.gutterWidth;
            }

            return size;
        },


        /**
         * Calculate the number of columns to be used. Gets css if using sizer element.
         * @param {number} [theContainerWidth] Optionally specify a container width if it's already available.
         */
        _setColumns : function( theContainerWidth ) {
            var containerWidth = theContainerWidth || this._getOuterWidth(this.$el[0]);
            var gutter = this._getGutterSize(containerWidth);
            var columnWidth = this._getColumnSize(gutter, containerWidth);
            var calculatedColumns = (containerWidth + gutter) / columnWidth;

            // Widths given from getComputedStyle are not precise enough...
            if ( Math.abs(Math.round(calculatedColumns) - calculatedColumns) < 0.03 ) {
                // e.g. calculatedColumns = 11.998876
                calculatedColumns = Math.round( calculatedColumns );
            }

            this.cols = Math.max( Math.floor(calculatedColumns), 1 );
            this.containerWidth = containerWidth;
            this.colWidth = columnWidth;
        },

        /**
         * Adjust the height of the grid
         */
        _setContainerSize : function() {
            var customData = Math.max.apply( Math, this.colYs)+5;
            this.$el.css( 'height', customData);
        },

        /**
         * Fire events with .shuffle namespace
         */
        _fire : function( name, args ) {
            this.$el.trigger( name + '.' + SHUFFLE, args && args.length ? args : [ this ] );
        },


        /**
         * Loops through each item that should be shown and calculates the x, y position.
         * @param {Array.<Element>} items Array of items that will be shown/layed out in order in their array.
         *     Because jQuery collection are always ordered in DOM order, we can't pass a jq collection.
         * @param {function} fn Callback function.
         * @param {boolean} isOnlyPosition If true this will position the items with zero opacity.
         */
        _layout : function( items, fn, isOnlyPosition ) {
            var self = this;

            fn = fn || self._filterEnd;

            $.each(items, function(index, item) {
                var $item = $(item);
                var itemData = $item.data();
                var currPos = itemData.position;
                var pos = self._getItemPosition( $item );

                // Save data for shrink
                $item.data( 'position', pos );

                // If the item will not change its position, do not add it to the render
                // queue. Transitions don't fire when setting a property to the same value.
                if ( pos.x === currPos.x && pos.y === currPos.y && itemData.scale === DEFAULT_SCALE ) {
                    return;
                }

                var transitionObj = {
                    $item: $item,
                    x: pos.x,
                    y: pos.y,
                    scale: DEFAULT_SCALE
                };

                if ( isOnlyPosition ) {
                    transitionObj.skipTransition = true;
                    transitionObj.opacity = 0;
                } else {
                    transitionObj.opacity = 1;
                    transitionObj.callback = fn;
                }

                self.styleQueue.push( transitionObj );
                self._layoutList.push( $item[0] );
            });

            // `_layout` always happens after `_shrink`, so it's safe to process the style
            // queue here with styles from the shrink method
            self._processStyleQueue();

            // Adjust the height of the container
            self._setContainerSize();
        },

        // Reset columns.
        _resetCols : function() {
            var i = this.cols;
            this.colYs = [];
            while (i--) {
                this.colYs.push( 0 );
            }
        },

        _reLayout : function() {
            this._resetCols();

            // If we've already sorted the elements, keep them sorted
            if ( this.lastSort ) {
                this.sort( this.lastSort, true );
            } else {
                this._layout( this.$items.filter('.filtered').get(), this._filterEnd );
            }
        },

        _getItemPosition : function( $item ) {
            var self = this;
            var itemWidth = self._getOuterWidth( $item[0], true );
            var columnSpan = itemWidth / self.colWidth;

            // If the difference between the rounded column span number and the
            // calculated column span number is really small, round the number to
            // make it fit.
            if ( Math.abs(Math.round(columnSpan) - columnSpan) < 0.03 ) {
                // e.g. columnSpan = 4.0089945390298745
                columnSpan = Math.round( columnSpan );
            }

            // How many columns does this item span. Ensure it's not more than the
            // amount of columns in the whole layout.
            var colSpan = Math.min( Math.ceil(columnSpan), self.cols );

            // The item spans only one column.
            if ( colSpan === 1 ) {
                return self._placeItem( $item, self.colYs );

                // The item spans more than one column, figure out how many different
                // places it could fit horizontally
            } else {
                var groupCount = self.cols + 1 - colSpan,
                    groupY = [],
                    groupColY,
                    i;

                // for each group potential horizontal position
                for ( i = 0; i < groupCount; i++ ) {
                    // make an array of colY values for that one group
                    groupColY = self.colYs.slice( i, i + colSpan );
                    // and get the max value of the array
                    groupY[i] = Math.max.apply( Math, groupColY );
                }

                return self._placeItem( $item, groupY );
            }
        },

        // worker method that places item in the columnSet with the the minY
        _placeItem : function( $item, setY ) {
            // get the minimum Y value from the columns
            var self = this,
                minimumY = Math.min.apply( Math, setY ),
                shortCol = 0;

            // Find index of short column, the first from the left where this item will go
            // if ( setY[i] === minimumY ) requires items' height to be exact every time.
            // The buffer value is very useful when the height is a percentage of the width
            for (var i = 0, len = setY.length; i < len; i++) {
                if ( setY[i] >= minimumY - self.buffer && setY[i] <= minimumY + self.buffer ) {
                    shortCol = i;
                    break;
                }
            }

            // Position the item
            var position = {
                x: Math.round( (self.colWidth * shortCol) + self.offset.left ),
                y: Math.round( minimumY + self.offset.top )
            };

            // Apply setHeight to necessary columns
            var setHeight = minimumY + self._getOuterHeight( $item[0], true ),
                setSpan = self.cols + 1 - len;
            for ( i = 0; i < setSpan; i++ ) {
                self.colYs[ shortCol + i ] = setHeight;
            }

            return position;
        },

        /**
         * Hides the elements that don't match our filter.
         * @param {jQuery} $collection jQuery collection to shrink.
         * @param {Function} fn Callback function.
         * @private
         */
        _shrink : function( $collection, fn ) {
            var self = this,
                $concealed = $collection || self.$items.filter('.concealed');

            fn = fn || self._shrinkEnd;

            // Abort if no items
            if ( !$concealed.length ) {
                return;
            }

            self._fire( Shuffle.EventType.SHRINK );

            $concealed.each(function() {
                var $item = $(this);
                var itemData = $item.data();
                var alreadyShrunk = itemData.scale === CONCEALED_SCALE;

                // Continuing would add a transitionend event listener to the element, but
                // that listener would execute because the transform and opacity would
                // stay the same.
                if ( alreadyShrunk ) {
                    return;
                }

                var transitionObj = {
                    $item: $item,
                    x: itemData.position.x,
                    y: itemData.position.y,
                    scale : CONCEALED_SCALE,
                    opacity: 0,
                    callback: fn
                };

                self.styleQueue.push( transitionObj );
                self._shrinkList.push( $item[0] );
            });
        },

        _onResize : function() {
            // If shuffle is disabled, destroyed, don't do anything
            if ( !this.enabled || this.destroyed ) {
                return;
            }

            // Will need to check height in the future if it's layed out horizontaly
            var containerWidth = this._getOuterWidth(this.$el[0]);

            // containerWidth hasn't changed, don't do anything
            if ( containerWidth === this.containerWidth ) {
                return;
            }

            this.resized();
        },


        /**
         * If the browser has 3d transforms available, build a string with those,
         * otherwise use 2d transforms.
         * @param {number} x X position.
         * @param {number} y Y position.
         * @param {number} scale Scale amount.
         * @return {string} A normalized string which can be used with the transform style.
         * @private
         */
        _getItemTransformString : function(x, y, scale) {
            if ( HAS_TRANSFORMS_3D ) {
                return 'translate3d(' + x + 'px, ' + y + 'px, 0) scale3d(' + scale + ', ' + scale + ', 1)';
            } else {
                return 'translate(' + x + 'px, ' + y + 'px) scale(' + scale + ', ' + scale + ')';
            }
        },

        _getStylesForTransition : function( opts ) {
            var styles = {
                opacity: opts.opacity
            };

            if ( this.supported ) {
                if ( opts.x !== undefined ) {
                    styles[ TRANSFORM ] = this._getItemTransformString( opts.x, opts.y, opts.scale );
                }
            } else {
                styles.left = opts.x;
                styles.top = opts.y;
            }


            // Show the item if its opacity will be 1.
            if ( opts.opacity === 1 ) {
                styles.visibility = 'visible';
            }

            return styles;
        },

        /**
         * Transitions an item in the grid
         *
         * @param {Object}   opts options
         * @param {jQuery}   opts.$item jQuery object representing the current item
         * @param {number}   opts.x translate's x
         * @param {number}   opts.y translate's y
         * @param {number}   opts.scale amount to scale the item
         * @param {number}   opts.opacity opacity of the item
         * @param {Function} opts.callback complete function for the animation
         * @private
         */
        _transition : function( opts ) {
            opts.$item.data('scale', opts.scale);

            var styles = this._getStylesForTransition( opts );
            this._startItemAnimation( opts.$item, styles, opts.callback );
        },


        _startItemAnimation : function( $item, styles, callback ) {
            var willBeVisible = styles.opacity === 1;
            var complete = $.proxy( this._handleItemAnimationEnd, this,
                callback || $.noop, $item[0], willBeVisible );

            // Use CSS Transforms if we have them
            if ( this.supported ) {

                $item.css( styles );

                // Transitions are not set until shuffle has loaded to avoid the initial transition.
                if ( this.initialized ) {
                    // Namespaced because the reveal appended function also wants to know
                    // about the transition end event.
                    $item.on( TRANSITIONEND + '.shuffleitem', complete );
                } else {
                    complete();
                }

                // Use jQuery to animate left/top
            } else {
                // jQuery cannot animate visibility, set it immediately.
                if ( 'visibility' in styles ) {
                    $item.css('visibility', styles.visibility);
                    delete styles.visibility;
                }
                $item.stop( true ).animate( styles, this.speed, 'swing', complete );
            }
        },


        _handleItemAnimationEnd : function( callback, item, willBeVisible, evt ) {
            // Make sure this event handler has not bubbled up from a child.
            if ( evt ) {
                if ( evt.target === item ) {
                    $( item ).off( '.shuffleitem' );
                } else {
                    return;
                }
            }

            if ( this._layoutList.length > 0 && $.inArray( item, this._layoutList ) > -1 ) {
                this._fire( Shuffle.EventType.LAYOUT );
                callback.call( this );
                this._layoutList.length = 0;
            } else if ( this._shrinkList.length > 0 && $.inArray( item, this._shrinkList ) > -1 ) {
                callback.call( this );
                this._shrinkList.length = 0;
            }

            if ( !willBeVisible ) {
                item.style.visibility = 'hidden';
            }
        },

        _processStyleQueue : function() {
            var self = this;

            $.each(this.styleQueue, function(i, transitionObj) {

                if ( transitionObj.skipTransition ) {
                    self._skipTransition(transitionObj.$item[0], function() {
                        transitionObj.$item.css( self._getStylesForTransition( transitionObj ) );
                    });
                } else {
                    self._transition( transitionObj );
                }
            });

            // Remove everything in the style queue
            self.styleQueue.length = 0;
        },

        _shrinkEnd : function() {
            this._fire( Shuffle.EventType.SHRUNK );
        },

        _filterEnd : function() {
            this._fire( Shuffle.EventType.FILTERED );
        },

        _sortEnd : function() {
            this._fire( Shuffle.EventType.SORTED );
        },

        /**
         * Change a property or execute a function which will not have a transition
         * @param {Element} element DOM element that won't be transitioned
         * @param {(string|Function)} property The new style property which will be set or a function which will be called
         * @param {string} [value] The value that `property` should be.
         * @private
         */
        _skipTransition : function( element, property, value ) {
            var duration = element.style[ TRANSITION_DURATION ];

            // Set the duration to zero so it happens immediately
            element.style[ TRANSITION_DURATION ] = '0ms'; // ms needed for firefox!

            if ( $.isFunction( property ) ) {
                property();
            } else {
                element.style[ property ] = value;
            }

            // Force reflow
            var reflow = element.offsetWidth;
            // Avoid jshint warnings: unused variables and expressions.
            reflow = null;

            // Put the duration back
            element.style[ TRANSITION_DURATION ] = duration;
        },

        _addItems : function( $newItems, animateIn, isSequential ) {
            var self = this;

            if ( !self.supported ) {
                animateIn = false;
            }

            $newItems.addClass('shuffle-item');
            self._initItems( $newItems );
            self._setTransitions( $newItems );
            self.$items = self._getItems();

            // Hide all items
            $newItems.css('opacity', 0);

            // Get ones that passed the current filter
            var $passed = self._filter( undefined, $newItems );
            var passed = $passed.get();

            // How many filtered elements?
            self._updateItemCount();

            if ( animateIn ) {
                self._layout( passed, null, true );

                if ( isSequential ) {
                    self._setSequentialDelay( $passed );
                }

                self._revealAppended( $passed );
            } else {
                self._layout( passed );
            }
        },

        _revealAppended : function( $newFilteredItems ) {
            var self = this;

            setTimeout(function() {
                $newFilteredItems.each(function(i, el) {
                    self._transition({
                        $item: $(el),
                        opacity: 1,
                        scale: DEFAULT_SCALE
                    });
                });
            }, self.revealAppendedDelay);
        },


        /**
         * Public Methods
         */

        /**
         * The magic. This is what makes the plugin 'shuffle'
         * @param {(string|Function)} [category] Category to filter by. Can be a function
         * @param {Object} [sortObj] A sort object which can sort the filtered set
         */
        shuffle : function( category, sortObj ) {
            var self = this;

            if ( !self.enabled ) {
                return;
            }

            if ( !category ) {
                category = ALL_ITEMS;
            }

            self._filter( category );
            // Save the last filter in case elements are appended.
            self.lastFilter = category;

            // How many filtered elements?
            self._updateItemCount();

            // Shrink each concealed item
            self._shrink();

            // If given a valid sort object, save it so that _reLayout() will sort the items
            if ( sortObj ) {
                self.lastSort = sortObj;
            }
            // Update transforms on .filtered elements so they will animate to their new positions
            self._reLayout();
        },

        /**
         * Gets the .filtered elements, sorts them, and passes them to layout
         *
         * @param {Object} opts the options object for the sorted plugin
         * @param {boolean} [fromFilter] was called from Shuffle.filter method.
         */
        sort : function( opts, fromFilter ) {
            var self = this,
                items = self.$items.filter('.filtered').sorted(opts);

            if ( !fromFilter ) {
                self._resetCols();
            }

            self._layout(items, function() {
                if (fromFilter) {
                    self._filterEnd();
                }
                self._sortEnd();
            });

            self.lastSort = opts;
        },

        /**
         * Relayout everything
         */
        resized : function( isOnlyLayout ) {
            if ( this.enabled ) {

                if ( !isOnlyLayout ) {
                    // Get updated colCount
                    this._setColumns();
                }

                // Layout items
                this._reLayout();
            }
        },

        /**
         * Use this instead of `update()` if you don't need the columns and gutters updated
         * Maybe an image inside `shuffle` loaded (and now has a height), which means calculations
         * could be off.
         */
        layout : function() {
            this.update( true );
        },

        update : function( isOnlyLayout ) {
            this.resized( isOnlyLayout );
        },

        /**
         * New items have been appended to shuffle. Fade them in sequentially
         * @param {jQuery} $newItems jQuery collection of new items
         * @param {boolean} [animateIn] If false, the new items won't animate in
         * @param {boolean} [isSequential] If false, new items won't sequentially fade in
         */
        appended : function( $newItems, animateIn, isSequential ) {
            // True if undefined
            animateIn = animateIn === false ? false : true;
            isSequential = isSequential === false ? false : true;

            this._addItems( $newItems, animateIn, isSequential );
        },

        /**
         * Disables shuffle from updating dimensions and layout on resize
         */
        disable : function() {
            this.enabled = false;
        },

        /**
         * Enables shuffle again
         * @param {boolean} [isUpdateLayout=true] if undefined, shuffle will update columns and gutters
         */
        enable : function( isUpdateLayout ) {
            this.enabled = true;
            if ( isUpdateLayout !== false ) {
                this.update();
            }
        },

        /**
         * Remove 1 or more shuffle items
         * @param {jQuery} $collection A jQuery object containing one or more element in shuffle
         * @return {Shuffle} The shuffle object
         */
        remove : function( $collection ) {

            // If this isn't a jquery object, exit
            if ( !$collection.length || !$collection.jquery ) {
                return;
            }

            var self = this;

            // Hide collection first
            self._shrink( $collection, function() {
                var shuffle = this;

                // Remove the collection in the callback
                $collection.remove();

                // Update the items, layout, count and fire off `removed` event
                setTimeout(function() {
                    shuffle.$items = shuffle._getItems();
                    shuffle.layout();
                    shuffle._updateItemCount();
                    shuffle._fire( Shuffle.EventType.REMOVED, [ $collection, shuffle ] );

                    // Let it get garbage collected
                    $collection = null;
                }, 0);
            });

            // Process changes
            self._processStyleQueue();

            return self;
        },

        /**
         * Destroys shuffle, removes events, styles, and classes
         */
        destroy : function() {
            var self = this;

            // If there is more than one shuffle instance on the page,
            // removing the resize handler from the window would remove them
            // all. This is why a unique value is needed.
            self.$window.off('.' + self.unique);

            // Reset container styles
            self.$el
                .removeClass( SHUFFLE )
                .removeAttr('style')
                .removeData( SHUFFLE );

            // Reset individual item styles
            self.$items
                .removeAttr('style')
                .removeClass('concealed filtered shuffle-item');

            // Null DOM references
            self.$window = null;
            self.$items = null;
            self.$el = null;
            self.$sizer = null;
            self.sizer = null;

            // Set a flag so if a debounced resize has been triggered,
            // it can first check if it is actually destroyed and not doing anything
            self.destroyed = true;
        }
    };


// Overrideable options
    Shuffle.options = {
        group: ALL_ITEMS, // Filter group
        speed: 250, // Transition/animation speed (milliseconds)
        easing: 'ease-out', // css easing function to use
        itemSelector: '', // e.g. '.picture-item'
        sizer: null, // sizer element. Can be anything columnWidth is
        gutterWidth: 0, // a static number or function that tells the plugin how wide the gutters between columns are (in pixels)
        columnWidth: 0, // a static number or function that returns a number which tells the plugin how wide the columns are (in pixels)
        delimeter: null, // if your group is not json, and is comma delimeted, you could set delimeter to ','
        buffer: 0, // useful for percentage based heights when they might not always be exactly the same (in pixels)
        initialSort: null, // Shuffle can be initialized with a sort object. It is the same object given to the sort method
        throttle: throttle, // By default, shuffle will try to throttle the resize event. This option will change the method it uses
        throttleTime: 300, // How often shuffle can be called on resize (in milliseconds)
        sequentialFadeDelay: 150, // Delay between each item that fades in when adding items
        supported: CAN_TRANSITION_TRANSFORMS // supports transitions and transforms
    };


// Not overrideable
    Shuffle.settings = {
        $sizer: null,
        useSizer: false,
        itemCss : { // default CSS for each item
            position: 'absolute',
            top: 0,
            left: 0
        },
        offset: { top: 0, left: 0 },
        revealAppendedDelay: 300,
        enabled: true,
        destroyed: false,
        initialized: false,
        styleQueue: []
    };


// Plugin definition
    $.fn.shuffle = function( opts ) {
        var args = Array.prototype.slice.call( arguments, 1 );
        return this.each(function() {
            var $this = $( this ),
                shuffle = $this.data( SHUFFLE );

            // If we don't have a stored shuffle, make a new one and save it
            if ( !shuffle ) {
                shuffle = new Shuffle( $this, opts );
                $this.data( SHUFFLE, shuffle );
            }

            if ( typeof opts === 'string' && shuffle[ opts ] ) {
                shuffle[ opts ].apply( shuffle, args );
            }
        });
    };


// You can return `undefined` from the `by` function to revert to DOM order
// This plugin does NOT return a jQuery object. It returns a plain array because
// jQuery sorts everything in DOM order.
    $.fn.sorted = function(options) {
        var opts = $.extend({}, $.fn.sorted.defaults, options),
            arr = this.get(),
            revert = false;

        if ( !arr.length ) {
            return [];
        }

        if ( opts.randomize ) {
            return $.fn.sorted.randomize( arr );
        }

        // Sort the elements by the opts.by function.
        // If we don't have opts.by, default to DOM order
        if (opts.by !== $.noop && opts.by !== null && opts.by !== undefined) {
            arr.sort(function(a, b) {

                // Exit early if we already know we want to revert
                if ( revert ) {
                    return 0;
                }

                var valA = opts.by($(a)),
                    valB = opts.by($(b));

                // If both values are undefined, use the DOM order
                if ( valA === undefined && valB === undefined ) {
                    revert = true;
                    return 0;
                }

                if ( valA === 'sortFirst' || valB === 'sortLast' ) {
                    return -1;
                }

                if ( valA === 'sortLast' || valB === 'sortFirst' ) {
                    return 1;
                }

                return (valA < valB) ? -1 :
                    (valA > valB) ? 1 : 0;
            });
        }

        // Revert to the original array if necessary
        if ( revert ) {
            return this.get();
        }

        if ( opts.reverse ) {
            arr.reverse();
        }

        return arr;
    };


    $.fn.sorted.defaults = {
        reverse: false, // Use array.reverse() to reverse the results
        by: null, // Sorting function
        randomize: false // If true, this will skip the sorting and return a randomized order in the array
    };


// http://stackoverflow.com/a/962890/373422
    $.fn.sorted.randomize = function( array ) {
        var top = array.length,
            tmp, current;

        if ( !top ) {
            return array;
        }

        while ( --top ) {
            current = Math.floor( Math.random() * (top + 1) );
            tmp = array[ current ];
            array[ current ] = array[ top ];
            array[ top ] = tmp;
        }

        return array;
    };

    return Shuffle;

});
